* The implementation of buildStringCast() in Wikimedia\Rdbms\Database has
changed to explicitly cast. Subclasses relying on the base-class
implementation should check whether they need to override it now.
+* BagOStuff::add is now abstract and must explicitly be defined in subclasses.
== Compatibility ==
MediaWiki 1.33 requires PHP 7.0.13 or later. Although HHVM 3.18.5 or later is
}
}
- public function upsert( $table, array $rows, array $uniqueIndexes, array $set,
+ public function upsert( $table, array $rows, $uniqueIndexes, array $set,
$fname = __METHOD__
) {
if ( $rows === [] ) {
$mainConfig->get( 'DBmwschema' ),
$mainConfig->get( 'DBprefix' )
),
- 'profiler' => Profiler::instance(),
+ 'profiler' => function ( $section ) {
+ return Profiler::instance()->scopedProfileIn( $section );
+ },
'trxProfiler' => Profiler::instance()->getTransactionProfiler(),
'replLogger' => LoggerFactory::getInstance( 'DBReplication' ),
'queryLogger' => LoggerFactory::getInstance( 'DBQuery' ),
'wanCache' => MediaWikiServices::getInstance()->getMainWANObjectCache(),
'srvCache' => ObjectCache::getLocalServerInstance( 'hash' ),
'logger' => LoggerFactory::getInstance( 'FileOperation' ),
- 'profiler' => Profiler::instance()
+ 'profiler' => function ( $section ) {
+ return Profiler::instance()->scopedProfileIn( $section );
+ }
];
$config['lockManager'] =
LockManagerGroup::singleton( $config['wikiId'] )->get( $config['lockManager'] );
protected $fileJournal;
/** @var LoggerInterface */
protected $logger;
- /** @var object|string Class name or object With profileIn/profileOut methods */
+ /** @var callable|null */
protected $profiler;
/** @var callable */
* - obResetFunc : alternative callback to clear the output buffer
* - streamMimeFunc : alternative method to determine the content type from the path
* - logger : Optional PSR logger object.
- * - profiler : Optional class name or object With profileIn/profileOut methods.
+ * - profiler : Optional callback that takes a section name argument and returns
+ * a ScopedCallback instance that ends the profile section in its destructor.
* @throws InvalidArgumentException
*/
public function __construct( array $config ) {
$this->statusWrapper = $config['statusWrapper'] ?? null;
$this->profiler = $config['profiler'] ?? null;
+ if ( !is_callable( $this->profiler ) ) {
+ $this->profiler = null;
+ }
$this->logger = $config['logger'] ?? new \Psr\Log\NullLogger();
$this->statusWrapper = $config['statusWrapper'] ?? null;
$this->tmpDirectory = $config['tmpDirectory'] ?? null;
* @return ScopedCallback|null
*/
protected function scopedProfileSection( $section ) {
- if ( $this->profiler ) {
- call_user_func( [ $this->profiler, 'profileIn' ], $section );
- return new ScopedCallback( [ $this->profiler, 'profileOut' ], [ $section ] );
- }
-
- return null;
+ return $this->profiler ? ( $this->profiler )( $section ) : null;
}
protected function resetOutputBuffer() {
return true;
}
+ public function add( $key, $value, $exptime = 0, $flags = 0 ) {
+ return apc_add(
+ $key . self::KEY_SUFFIX,
+ $this->setSerialize( $value ),
+ $exptime
+ );
+ }
+
protected function setSerialize( $value ) {
if ( !$this->nativeSerialize && !$this->isInteger( $value ) ) {
$value = serialize( $value );
return true;
}
+ public function add( $key, $value, $exptime = 0, $flags = 0 ) {
+ return apcu_add(
+ $key . self::KEY_SUFFIX,
+ $this->setSerialize( $value ),
+ $exptime
+ );
+ }
+
public function delete( $key, $flags = 0 ) {
apcu_delete( $key . self::KEY_SUFFIX );
const READ_LATEST = 1; // use latest data for replicated stores
const READ_VERIFIED = 2; // promise that caller can tell when keys are stale
/** Bitfield constants for set()/merge() */
- const WRITE_SYNC = 1; // synchronously write to all locations for replicated stores
- const WRITE_CACHE_ONLY = 2; // Only change state of the in-memory cache
+ const WRITE_SYNC = 4; // synchronously write to all locations for replicated stores
+ const WRITE_CACHE_ONLY = 8; // Only change state of the in-memory cache
/**
* $params include:
* @param string $key
* @param int $ttl Time-to-live (seconds)
* @param callable $callback Callback that derives the new value
- * @param int $flags Bitfield of BagOStuff::READ_* constants [optional]
+ * @param int $flags Bitfield of BagOStuff::READ_* or BagOStuff::WRITE_* constants [optional]
* @return mixed The cached value if found or the result of $callback otherwise
* @since 1.27
*/
}
$value = call_user_func( $callback );
if ( $value !== false ) {
- $this->set( $key, $value, $ttl );
+ $this->set( $key, $value, $ttl, $flags );
}
}
* @param callable $callback Callback method to be executed
* @param int $exptime Either an interval in seconds or a unix timestamp for expiry
* @param int $attempts The amount of times to attempt a merge in case of failure
+ * @param int $flags Bitfield of BagOStuff::WRITE_* constants
* @return bool Success
*/
- protected function mergeViaCas( $key, $callback, $exptime = 0, $attempts = 10 ) {
+ protected function mergeViaCas( $key, $callback, $exptime = 0, $attempts = 10, $flags = 0 ) {
do {
$this->clearLastError();
$reportDupes = $this->reportDupes;
$success = true; // do nothing
} elseif ( $currentValue === false ) {
// Try to create the key, failing if it gets created in the meantime
- $success = $this->add( $key, $value, $exptime );
+ $success = $this->add( $key, $value, $exptime, $flags );
} else {
// Try to update the key, failing if it gets changed in the meantime
- $success = $this->cas( $casToken, $key, $value, $exptime );
+ $success = $this->cas( $casToken, $key, $value, $exptime, $flags );
}
if ( $this->getLastError() ) {
$this->logger->warning(
* @param string $key
* @param mixed $value
* @param int $exptime Either an interval in seconds or a unix timestamp for expiry
+ * @param int $flags Bitfield of BagOStuff::WRITE_* constants
* @return bool Success
* @throws Exception
*/
- protected function cas( $casToken, $key, $value, $exptime = 0 ) {
+ protected function cas( $casToken, $key, $value, $exptime = 0, $flags = 0 ) {
if ( !$this->lock( $key, 0 ) ) {
return false; // non-blocking
}
$curCasToken = null; // passed by reference
$this->getWithToken( $key, $curCasToken, self::READ_LATEST );
if ( $casToken === $curCasToken ) {
- $success = $this->set( $key, $value, $exptime );
+ $success = $this->set( $key, $value, $exptime, $flags );
} else {
$this->logger->info(
__METHOD__ . ' failed due to race condition for {key}.',
*
* @param string $key
* @param int $expiry
+ * @param int $flags Bitfield of BagOStuff::WRITE_* constants (since 1.33)
* @return bool Success Returns false if there is no key
* @since 1.28
*/
- public function changeTTL( $key, $expiry = 0 ) {
+ public function changeTTL( $key, $expiry = 0, $flags = 0 ) {
$value = $this->get( $key );
- return ( $value === false ) ? false : $this->set( $key, $value, $expiry );
+ return ( $value === false ) ? false : $this->set( $key, $value, $expiry, $flags );
}
/**
function () use ( $key, $expiry, $fname ) {
$this->clearLastError();
if ( $this->add( "{$key}:lock", 1, $expiry ) ) {
- return true; // locked!
+ return WaitConditionLoop::CONDITION_REACHED; // locked!
} elseif ( $this->getLastError() ) {
$this->logger->warning(
$fname . ' failed due to I/O error for {key}.',
/**
* Get an associative array containing the item for each of the keys that have items.
- * @param array $keys List of strings
+ * @param string[] $keys List of keys
* @param int $flags Bitfield; supports READ_LATEST [optional]
* @return array
*/
public function getMulti( array $keys, $flags = 0 ) {
$res = [];
foreach ( $keys as $key ) {
- $val = $this->get( $key );
+ $val = $this->get( $key, $flags );
if ( $val !== false ) {
$res[$key] = $val;
}
}
+
return $res;
}
/**
- * Batch insertion
- * @param array $data $key => $value assoc array
+ * Batch insertion/replace
+ * @param mixed[] $data Map of (key => value)
* @param int $exptime Either an interval in seconds or a unix timestamp for expiry
+ * @param int $flags Bitfield of BagOStuff::WRITE_* constants (since 1.33)
* @return bool Success
* @since 1.24
*/
- public function setMulti( array $data, $exptime = 0 ) {
+ public function setMulti( array $data, $exptime = 0, $flags = 0 ) {
$res = true;
foreach ( $data as $key => $value ) {
- if ( !$this->set( $key, $value, $exptime ) ) {
+ if ( !$this->set( $key, $value, $exptime, $flags ) ) {
$res = false;
}
}
+
+ return $res;
+ }
+
+ /**
+ * Batch deletion
+ * @param string[] $keys List of keys
+ * @param int $flags Bitfield of BagOStuff::WRITE_* constants
+ * @return bool Success
+ * @since 1.33
+ */
+ public function deleteMulti( array $keys, $flags = 0 ) {
+ $res = true;
+ foreach ( $keys as $key ) {
+ $res = $this->delete( $key, $flags ) && $res;
+ }
+
return $res;
}
/**
+ * Insertion
* @param string $key
* @param mixed $value
* @param int $exptime
+ * @param int $flags Bitfield of BagOStuff::WRITE_* constants (since 1.33)
* @return bool Success
*/
- public function add( $key, $value, $exptime = 0 ) {
- // @note: avoid lock() here since that method uses *this* method by default
- if ( $this->get( $key ) === false ) {
- return $this->set( $key, $value, $exptime );
- }
- return false; // key already set
- }
+ abstract public function add( $key, $value, $exptime = 0, $flags = 0 );
/**
* Increase stored value of $key by $value while preserving its TTL
if ( !$this->lock( $key, 1 ) ) {
return false;
}
- $n = $this->get( $key );
+ $n = $this->get( $key, self::READ_LATEST );
if ( $this->isInteger( $n ) ) { // key exists?
$n += intval( $value );
$this->set( $key, max( 0, $n ) ); // exptime?
// These just call the backend (tested elsewhere)
// @codeCoverageIgnoreStart
+ public function add( $key, $value, $exptime = 0, $flags = 0 ) {
+ if ( $this->get( $key ) === false ) {
+ return $this->set( $key, $value, $exptime, $flags );
+ }
+
+ return false; // key already set
+ }
+
public function lock( $key, $timeout = 6, $expiry = 6, $rclass = '' ) {
return $this->backend->lock( $key, $timeout, $expiry, $rclass );
}
return false;
}
- public function add( $key, $value, $exp = 0 ) {
+ public function add( $key, $value, $exp = 0, $flags = 0 ) {
return true;
}
return true;
}
+ public function add( $key, $value, $exptime = 0, $flags = 0 ) {
+ if ( $this->get( $key ) === false ) {
+ return $this->set( $key, $value, $exptime, $flags );
+ }
+
+ return false; // key already set
+ }
+
public function delete( $key, $flags = 0 ) {
unset( $this->bag[$key] );
$this->fixExpiry( $exptime ) );
}
- protected function cas( $casToken, $key, $value, $exptime = 0 ) {
+ protected function cas( $casToken, $key, $value, $exptime = 0, $flags = 0 ) {
return $this->client->cas( $casToken, $this->validateKeyEncoding( $key ),
$value, $this->fixExpiry( $exptime ) );
}
return $this->client->delete( $this->validateKeyEncoding( $key ) );
}
- public function add( $key, $value, $exptime = 0 ) {
+ public function add( $key, $value, $exptime = 0, $flags = 0 ) {
return $this->client->add( $this->validateKeyEncoding( $key ), $value,
$this->fixExpiry( $exptime ) );
}
return $this->mergeViaCas( $key, $callback, $exptime, $attempts );
}
- public function changeTTL( $key, $exptime = 0 ) {
+ public function changeTTL( $key, $exptime = 0, $flags = 0 ) {
return $this->client->touch( $this->validateKeyEncoding( $key ),
$this->fixExpiry( $exptime ) );
}
public function set( $key, $value, $exptime = 0, $flags = 0 ) {
$this->debugLog( "set($key)" );
- $result = parent::set( $key, $value, $exptime );
+ $result = parent::set( $key, $value, $exptime, $flags = 0 );
if ( $result === false && $this->client->getResultCode() === Memcached::RES_NOTSTORED ) {
// "Not stored" is always used as the mcrouter response with AllAsyncRoute
return true;
return $this->checkResult( $key, $result );
}
- protected function cas( $casToken, $key, $value, $exptime = 0 ) {
+ protected function cas( $casToken, $key, $value, $exptime = 0, $flags = 0 ) {
$this->debugLog( "cas($key)" );
- return $this->checkResult( $key, parent::cas( $casToken, $key, $value, $exptime ) );
+ return $this->checkResult( $key, parent::cas( $casToken, $key, $value, $exptime, $flags ) );
}
public function delete( $key, $flags = 0 ) {
return $this->checkResult( $key, $result );
}
- public function add( $key, $value, $exptime = 0 ) {
+ public function add( $key, $value, $exptime = 0, $flags = 0 ) {
$this->debugLog( "add($key)" );
return $this->checkResult( $key, parent::add( $key, $value, $exptime ) );
}
return $this->checkResult( false, $result );
}
- /**
- * @param array $data
- * @param int $exptime
- * @return bool
- */
- public function setMulti( array $data, $exptime = 0 ) {
+ public function setMulti( array $data, $exptime = 0, $flags = 0 ) {
$this->debugLog( 'setMulti(' . implode( ', ', array_keys( $data ) ) . ')' );
foreach ( array_keys( $data ) as $key ) {
$this->validateKeyEncoding( $key );
return $this->checkResult( false, $result );
}
- public function changeTTL( $key, $expiry = 0 ) {
+ public function changeTTL( $key, $expiry = 0, $flags = 0 ) {
$this->debugLog( "touch($key)" );
$result = $this->client->touch( $key, $expiry );
return $this->checkResult( $key, $result );
return $this->doWrite( $this->cacheIndexes, $this->asyncWrites, 'delete', $key );
}
- public function add( $key, $value, $exptime = 0 ) {
+ public function add( $key, $value, $exptime = 0, $flags = 0 ) {
// Try the write to the top-tier cache
$ok = $this->doWrite( [ 0 ], $this->asyncWrites, 'add', $key, $value, $exptime );
if ( $ok ) {
return $this->handleError( "Failed to store $key", $rcode, $rerr );
}
+ public function add( $key, $value, $exptime = 0, $flags = 0 ) {
+ if ( $this->get( $key ) === false ) {
+ return $this->set( $key, $value, $exptime, $flags );
+ }
+
+ return false; // key already set
+ }
+
public function delete( $key, $flags = 0 ) {
// @TODO: respect WRITE_SYNC (e.g. EACH_QUORUM)
$req = [
return $result;
}
- /**
- * @param array $data
- * @param int $expiry
- * @return bool
- */
- public function setMulti( array $data, $expiry = 0 ) {
+ public function setMulti( array $data, $expiry = 0, $flags = 0 ) {
$batches = [];
$conns = [];
foreach ( $data as $key => $value ) {
return $result;
}
- public function add( $key, $value, $expiry = 0 ) {
+ public function deleteMulti( array $keys, $flags = 0 ) {
+ $batches = [];
+ $conns = [];
+ foreach ( $keys as $key ) {
+ list( $server, $conn ) = $this->getConnection( $key );
+ if ( !$conn ) {
+ continue;
+ }
+ $conns[$server] = $conn;
+ $batches[$server][] = $key;
+ }
+
+ $result = true;
+ foreach ( $batches as $server => $batchKeys ) {
+ $conn = $conns[$server];
+ try {
+ $conn->multi( Redis::PIPELINE );
+ foreach ( $batchKeys as $key ) {
+ $conn->delete( $key );
+ }
+ $batchResult = $conn->exec();
+ if ( $batchResult === false ) {
+ $this->debug( "deleteMulti request to $server failed" );
+ continue;
+ }
+ foreach ( $batchResult as $value ) {
+ if ( $value === false ) {
+ $result = false;
+ }
+ }
+ } catch ( RedisException $e ) {
+ $this->handleException( $conn, $e );
+ $result = false;
+ }
+ }
+
+ return $result;
+ }
+
+ public function add( $key, $value, $expiry = 0, $flags = 0 ) {
list( $server, $conn ) = $this->getConnection( $key );
if ( !$conn ) {
return false;
return $result;
}
- public function changeTTL( $key, $expiry = 0 ) {
+ public function changeTTL( $key, $expiry = 0, $flags = 0 ) {
list( $server, $conn ) = $this->getConnection( $key );
if ( !$conn ) {
return false;
return $this->writeStore->set( $key, $value, $exptime, $flags );
}
+ public function setMulti( array $keys, $exptime = 0, $flags = 0 ) {
+ return $this->writeStore->setMulti( $keys, $exptime, $flags );
+ }
+
public function delete( $key, $flags = 0 ) {
return $this->writeStore->delete( $key, $flags );
}
- public function add( $key, $value, $exptime = 0 ) {
- return $this->writeStore->add( $key, $value, $exptime );
+ public function deleteMulti( array $keys, $flags = 0 ) {
+ return $this->writeStore->deleteMulti( $keys, $flags );
+ }
+
+ public function add( $key, $value, $exptime = 0, $flags = 0 ) {
+ return $this->writeStore->add( $key, $value, $exptime, $flags );
}
public function incr( $key, $value = 1 ) {
public function set( $key, $value, $expire = 0, $flags = 0 ) {
$result = wincache_ucache_set( $key, serialize( $value ), $expire );
- /* wincache_ucache_set returns an empty array on success if $value
- * was an array, bool otherwise */
- return ( is_array( $result ) && $result === [] ) || $result;
+ return ( $result === [] || $result === true );
+ }
+
+ public function add( $key, $value, $exptime = 0, $flags = 0 ) {
+ $result = wincache_ucache_add( $key, serialize( $value ), $exptime );
+
+ return ( $result === [] || $result === true );
}
public function delete( $key, $flags = 0 ) {
}
public function upsert(
- $table, array $rows, array $uniqueIndexes, array $set, $fname = __METHOD__
+ $table, array $rows, $uniqueIndexes, array $set, $fname = __METHOD__
) {
return $this->__call( __FUNCTION__, func_get_args() );
}
/** @var int[] Prior flags member variable values */
private $priorFlags = [];
- /** @var mixed Class name or object With profileIn/profileOut methods */
+ /** @var callable|null */
protected $profiler;
/** @var TransactionProfiler */
protected $trxProfiler;
$this->srvCache = $params['srvCache'] ?? new HashBagOStuff();
- $this->profiler = $params['profiler'];
+ $this->profiler = is_callable( $params['profiler'] ) ? $params['profiler'] : null;
$this->trxProfiler = $params['trxProfiler'];
$this->connLogger = $params['connLogger'];
$this->queryLogger = $params['queryLogger'];
* used to adjust lock timeouts or encoding modes and the like.
* - connLogger: Optional PSR-3 logger interface instance.
* - queryLogger: Optional PSR-3 logger interface instance.
- * - profiler: Optional class name or object with profileIn()/profileOut() methods.
- * These will be called in query(), using a simplified version of the SQL that also
- * includes the agent as a SQL comment.
+ * - profiler : Optional callback that takes a section name argument and returns
+ * a ScopedCallback instance that ends the profile section in its destructor.
+ * These will be called in query(), using a simplified version of the SQL that
+ * also includes the agent as a SQL comment.
* - trxProfiler: Optional TransactionProfiler instance.
* - errorLogger: Optional callback that takes an Exception and logs it.
* - deprecationLogger: Optional callback that takes a string and logs it.
}
/**
- * Make sure isOpen() returns true as a sanity check
+ * Make sure there is an open connection handle (alive or not) as a sanity check
+ *
+ * This guards against fatal errors to the binding handle not being defined
+ * in cases where open() was never called or close() was already called
*
* @throws DBUnexpectedError
*/
- protected function assertOpen() {
+ protected function assertHasConnectionHandle() {
if ( !$this->isOpen() ) {
throw new DBUnexpectedError( $this, "DB connection was already closed." );
}
}
+ /**
+ * Make sure that this server is not marked as a replica nor read-only as a sanity check
+ *
+ * @throws DBUnexpectedError
+ */
+ protected function assertIsWritableMaster() {
+ if ( $this->getLBInfo( 'replica' ) === true ) {
+ throw new DBUnexpectedError(
+ $this,
+ 'Write operations are not allowed on replica database connections.'
+ );
+ }
+ $reason = $this->getReadOnlyReason();
+ if ( $reason !== false ) {
+ throw new DBReadOnlyError( $this, "Database is read-only: $reason" );
+ }
+ }
+
/**
* Closes underlying database connection
* @since 1.20
public function query( $sql, $fname = __METHOD__, $tempIgnore = false ) {
$this->assertTransactionStatus( $sql, $fname );
+ $this->assertHasConnectionHandle();
- # Avoid fatals if close() was called
- $this->assertOpen();
-
+ $priorTransaction = $this->trxLevel;
$priorWritesPending = $this->writesOrCallbacksPending();
$this->lastQuery = $sql;
- $isWrite = $this->isWriteQuery( $sql );
- if ( $isWrite ) {
- $isNonTempWrite = !$this->registerTempTableOperation( $sql );
- } else {
- $isNonTempWrite = false;
- }
-
- if ( $isWrite ) {
- if ( $this->getLBInfo( 'replica' ) === true ) {
- throw new DBError(
- $this,
- 'Write operations are not allowed on replica database connections.'
- );
- }
+ if ( $this->isWriteQuery( $sql ) ) {
# In theory, non-persistent writes are allowed in read-only mode, but due to things
# like https://bugs.mysql.com/bug.php?id=33669 that might not work anyway...
- $reason = $this->getReadOnlyReason();
- if ( $reason !== false ) {
- throw new DBReadOnlyError( $this, "Database is read-only: $reason" );
- }
- # Set a flag indicating that writes have been done
- $this->lastWriteTime = microtime( true );
+ $this->assertIsWritableMaster();
+ # Avoid treating temporary table operations as meaningful "writes"
+ $isEffectiveWrite = !$this->registerTempTableOperation( $sql );
+ } else {
+ $isEffectiveWrite = false;
}
# Add trace comment to the begin of the sql string, right after the operator.
# Or, for one-word queries (like "BEGIN" or COMMIT") add it to the end (T44598)
$commentedSql = preg_replace( '/\s|$/', " /* $fname {$this->agent} */ ", $sql, 1 );
- # Start implicit transactions that wrap the request if DBO_TRX is enabled
- if ( !$this->trxLevel && $this->getFlag( self::DBO_TRX )
- && $this->isTransactableQuery( $sql )
- ) {
- $this->begin( __METHOD__ . " ($fname)", self::TRANSACTION_INTERNAL );
- $this->trxAutomatic = true;
- }
-
- # Keep track of whether the transaction has write queries pending
- if ( $this->trxLevel && !$this->trxDoneWrites && $isWrite ) {
- $this->trxDoneWrites = true;
- $this->trxProfiler->transactionWritingIn(
- $this->server, $this->getDomainID(), $this->trxShortId );
- }
-
- if ( $this->getFlag( self::DBO_DEBUG ) ) {
- $this->queryLogger->debug( "{$this->getDomainID()} {$commentedSql}" );
- }
-
# Send the query to the server and fetch any corresponding errors
- $ret = $this->doProfiledQuery( $sql, $commentedSql, $isNonTempWrite, $fname );
+ $ret = $this->attemptQuery( $sql, $commentedSql, $isEffectiveWrite, $fname );
$lastError = $this->lastError();
$lastErrno = $this->lastErrno();
- # Try reconnecting if the connection was lost
+ $recoverableSR = false; // recoverable statement rollback?
+ $recoverableCL = false; // recoverable connection loss?
+
if ( $ret === false && $this->wasConnectionLoss() ) {
- # Check if any meaningful session state was lost
- $recoverable = $this->canRecoverFromDisconnect( $sql, $priorWritesPending );
+ # Check if no meaningful session state was lost
+ $recoverableCL = $this->canRecoverFromDisconnect( $sql, $priorWritesPending );
# Update session state tracking and try to restore the connection
$reconnected = $this->replaceLostConnection( __METHOD__ );
# Silently resend the query to the server if it is safe and possible
- if ( $reconnected && $recoverable ) {
- $ret = $this->doProfiledQuery( $sql, $commentedSql, $isNonTempWrite, $fname );
+ if ( $recoverableCL && $reconnected ) {
+ $ret = $this->attemptQuery( $sql, $commentedSql, $isEffectiveWrite, $fname );
$lastError = $this->lastError();
$lastErrno = $this->lastErrno();
if ( $ret === false && $this->wasConnectionLoss() ) {
# Query probably causes disconnects; reconnect and do not re-run it
$this->replaceLostConnection( __METHOD__ );
+ } else {
+ $recoverableCL = false; // connection does not need recovering
+ $recoverableSR = $this->wasKnownStatementRollbackError();
}
}
+ } else {
+ $recoverableSR = $this->wasKnownStatementRollbackError();
}
if ( $ret === false ) {
- if ( $this->trxLevel ) {
- if ( $this->wasKnownStatementRollbackError() ) {
+ if ( $priorTransaction ) {
+ if ( $recoverableSR ) {
# We're ignoring an error that caused just the current query to be aborted.
# But log the cause so we can log a deprecation notice if a caller actually
# does ignore it.
$this->trxStatusIgnoredCause = [ $lastError, $lastErrno, $fname ];
- } else {
+ } elseif ( !$recoverableCL ) {
# Either the query was aborted or all queries after BEGIN where aborted.
# In the first case, the only options going forward are (a) ROLLBACK, or
# (b) ROLLBACK TO SAVEPOINT (if one was set). If the later case, the only
# option is ROLLBACK, since the snapshots would have been released.
$this->trxStatus = self::STATUS_TRX_ERROR;
$this->trxStatusCause =
- $this->makeQueryException( $lastError, $lastErrno, $sql, $fname );
+ $this->getQueryExceptionAndLog( $lastError, $lastErrno, $sql, $fname );
$tempIgnore = false; // cannot recover
$this->trxStatusIgnoredCause = null;
}
*
* @param string $sql Original SQL query
* @param string $commentedSql SQL query with debugging/trace comment
- * @param bool $isWrite Whether the query is a (non-temporary) write operation
+ * @param bool $isEffectiveWrite Whether the query is a (non-temporary table) write
* @param string $fname Name of the calling function
* @return bool|ResultWrapper True for a successful write query, ResultWrapper
* object for a successful read query, or false on failure
*/
- private function doProfiledQuery( $sql, $commentedSql, $isWrite, $fname ) {
+ private function attemptQuery( $sql, $commentedSql, $isEffectiveWrite, $fname ) {
+ $this->beginIfImplied( $sql, $fname );
+
+ # Keep track of whether the transaction has write queries pending
+ if ( $isEffectiveWrite ) {
+ $this->lastWriteTime = microtime( true );
+ if ( $this->trxLevel && !$this->trxDoneWrites ) {
+ $this->trxDoneWrites = true;
+ $this->trxProfiler->transactionWritingIn(
+ $this->server, $this->getDomainID(), $this->trxShortId );
+ }
+ }
+
+ if ( $this->getFlag( self::DBO_DEBUG ) ) {
+ $this->queryLogger->debug( "{$this->getDomainID()} {$commentedSql}" );
+ }
+
$isMaster = !is_null( $this->getLBInfo( 'master' ) );
# generalizeSQL() will probably cut down the query to reasonable
# logging size most of the time. The substr is really just a sanity check.
$queryProf .= $this->trxShortId ? " [TRX#{$this->trxShortId}]" : "";
$startTime = microtime( true );
- if ( $this->profiler ) {
- $this->profiler->profileIn( $queryProf );
- }
+ $ps = $this->profiler ? ( $this->profiler )( $queryProf ) : null;
$this->affectedRowCount = null;
$ret = $this->doQuery( $commentedSql );
$this->affectedRowCount = $this->affectedRows();
- if ( $this->profiler ) {
- $this->profiler->profileOut( $queryProf );
- }
+ unset( $ps ); // profile out (if set)
$queryRuntime = max( microtime( true ) - $startTime, 0.0 );
- unset( $queryProfSection ); // profile out (if set)
-
if ( $ret !== false ) {
$this->lastPing = $startTime;
- if ( $isWrite && $this->trxLevel ) {
+ if ( $isEffectiveWrite && $this->trxLevel ) {
$this->updateTrxWriteQueryTime( $sql, $queryRuntime, $this->affectedRows() );
$this->trxWriteCallers[] = $fname;
}
$this->trxProfiler->recordQueryCompletion(
$queryProf,
$startTime,
- $isWrite,
- $isWrite ? $this->affectedRows() : $this->numRows( $ret )
+ $isEffectiveWrite,
+ $isEffectiveWrite ? $this->affectedRows() : $this->numRows( $ret )
);
$this->queryLogger->debug( $sql, [
'method' => $fname,
return $ret;
}
+ /**
+ * Start an implicit transaction if DBO_TRX is enabled and no transaction is active
+ *
+ * @param string $sql
+ * @param string $fname
+ */
+ private function beginIfImplied( $sql, $fname ) {
+ if (
+ !$this->trxLevel &&
+ $this->getFlag( self::DBO_TRX ) &&
+ $this->isTransactableQuery( $sql )
+ ) {
+ $this->begin( __METHOD__ . " ($fname)", self::TRANSACTION_INTERNAL );
+ $this->trxAutomatic = true;
+ }
+ }
+
/**
* Update the estimated run-time of a query, not counting large row lock times
*
}
/**
- * Determine whether or not it is safe to retry queries after a database
- * connection is lost
+ * Determine whether it is safe to retry queries after a database connection is lost
*
* @param string $sql SQL query
* @param bool $priorWritesPending Whether there is a transaction open with
* Clean things up after transaction loss
*/
private function handleTransactionLoss() {
+ if ( $this->trxDoneWrites ) {
+ $this->trxProfiler->transactionWritingOut(
+ $this->server,
+ $this->getDomainID(),
+ $this->trxShortId,
+ $this->pendingWriteQueryDuration( self::ESTIMATE_TOTAL ),
+ $this->trxWriteAffectedRows
+ );
+ }
$this->trxLevel = 0;
$this->trxAtomicCounter = 0;
$this->trxIdleCallbacks = []; // T67263; transaction already lost
if ( $tempIgnore ) {
$this->queryLogger->debug( "SQL ERROR (ignored): $error\n" );
} else {
- $exception = $this->makeQueryException( $error, $errno, $sql, $fname );
+ $exception = $this->getQueryExceptionAndLog( $error, $errno, $sql, $fname );
throw $exception;
}
* @param string $fname
* @return DBError
*/
- private function makeQueryException( $error, $errno, $sql, $fname ) {
+ private function getQueryExceptionAndLog( $error, $errno, $sql, $fname ) {
$sql1line = mb_substr( str_replace( "\n", "\\n", $sql ), 0, 5 * 1024 );
$this->queryLogger->error(
"{fname}\t{db_server}\t{errno}\t{error}\t{sql1line}",
'error' => $error,
'sql1line' => $sql1line,
'fname' => $fname,
+ 'trace' => ( new RuntimeException() )->getTraceAsString()
] )
);
$this->queryLogger->debug( "SQL ERROR: " . $error . "\n" );
return;
}
+ $uniqueIndexes = (array)$uniqueIndexes;
// Single row case
if ( !is_array( reset( $rows ) ) ) {
$rows = [ $rows ];
$this->query( $sql, $fname );
}
- public function upsert( $table, array $rows, array $uniqueIndexes, array $set,
+ public function upsert( $table, array $rows, $uniqueIndexes, array $set,
$fname = __METHOD__
) {
if ( $rows === [] ) {
return true; // nothing to do
}
+ $uniqueIndexes = (array)$uniqueIndexes;
if ( !is_array( reset( $rows ) ) ) {
$rows = [ $rows ];
}
}
/**
- * @return bool Whether it is safe to assume the given error only caused statement rollback
+ * @return bool Whether it is known that the last query error only caused statement rollback
* @note This is for backwards compatibility for callers catching DBError exceptions in
* order to ignore problems like duplicate key errors or foriegn key violations
* @since 1.31
throw new DBUnexpectedError( $this, $msg );
}
- // Avoid fatals if close() was called
- $this->assertOpen();
+ $this->assertHasConnectionHandle();
$this->doBegin( $fname );
$this->trxStatus = self::STATUS_TRX_OK;
}
}
- // Avoid fatals if close() was called
- $this->assertOpen();
+ $this->assertHasConnectionHandle();
$this->runOnTransactionPreCommitCallbacks();
}
if ( $trxActive ) {
- // Avoid fatals if close() was called
- $this->assertOpen();
+ $this->assertHasConnectionHandle();
$this->doRollback( $fname );
$this->trxStatus = self::STATUS_TRX_NONE;
return $errno == 2062;
}
- /**
- * @param string $table
- * @param array $uniqueIndexes
- * @param array $rows
- * @param string $fname
- */
public function replace( $table, $uniqueIndexes, $rows, $fname = __METHOD__ ) {
$this->nativeReplace( $table, $rows, $fname );
}
$this->query( $sql, $fname );
}
- /**
- * @param string $table
- * @param array $rows
- * @param array $uniqueIndexes
- * @param array $set
- * @param string $fname
- * @return bool
- */
- public function upsert( $table, array $rows, array $uniqueIndexes,
- array $set, $fname = __METHOD__
+ public function upsert(
+ $table, array $rows, $uniqueIndexes, array $set, $fname = __METHOD__
) {
if ( $rows === [] ) {
return true; // nothing to do
* errors which wouldn't have occurred in MySQL.
*
* @param string $table The table to replace the row(s) in.
- * @param array $uniqueIndexes Either a list of fields that define a unique index or
- * an array of such lists if there are multiple unique indexes defined in the schema
+ * @param array[]|string[]|string $uniqueIndexes All unique indexes. One of the following:
+ * a) the one unique field in the table (when no composite unique key exist)
+ * b) a list of all unique fields in the table (when no composite unique key exist)
+ * c) a list of all unique indexes in the table (each as a list of the indexed fields)
* @param array $rows Can be either a single row to insert, or multiple rows,
* in the same format as for IDatabase::insert()
* @param string $fname Calling function name (use __METHOD__) for logs/profiling
*
* @param string $table Table name. This will be passed through Database::tableName().
* @param array $rows A single row or list of rows to insert
- * @param array $uniqueIndexes Either a list of fields that define a unique index or
- * an array of such lists if there are multiple unique indexes defined in the schema
+ * @param array[]|string[]|string $uniqueIndexes All unique indexes. One of the following:
+ * a) the one unique field in the table (when no composite unique key exist)
+ * b) a list of all unique fields in the table (when no composite unique key exist)
+ * c) a list of all unique indexes in the table (each as a list of the indexed fields)
* @param array $set An array of values to SET. For each array element, the
* key gives the field name, and the value gives the data to set that
* field to. The data will be quoted by IDatabase::addQuotes().
* @return bool Return true if no exception was thrown (deprecated since 1.33)
*/
public function upsert(
- $table, array $rows, array $uniqueIndexes, array $set, $fname = __METHOD__
+ $table, array $rows, $uniqueIndexes, array $set, $fname = __METHOD__
);
/**
return $values;
}
- public function setMulti( array $data, $expiry = 0 ) {
+ public function setMulti( array $data, $expiry = 0, $flags = 0 ) {
+ return $this->insertMulti( $data, $expiry, $flags, true );
+ }
+
+ private function insertMulti( array $data, $expiry, $flags, $replace ) {
$keysByTable = [];
foreach ( $data as $key => $value ) {
list( $serverIndex, $tableName ) = $this->getTableByKey( $key );
}
try {
- $db->replace(
- $tableName,
- [ 'keyname' ],
- $rows,
- __METHOD__
- );
+ if ( $replace ) {
+ $db->replace( $tableName, [ 'keyname' ], $rows, __METHOD__ );
+ } else {
+ $db->insert( $tableName, $rows, __METHOD__, [ 'IGNORE' ] );
+ $result = ( $db->affectedRows() > 0 && $result );
+ }
} catch ( DBError $e ) {
$this->handleWriteError( $e, $db, $serverIndex );
$result = false;
}
}
+ }
+ if ( ( $flags & self::WRITE_SYNC ) == self::WRITE_SYNC ) {
+ $result = $this->waitForReplication() && $result;
}
return $result;
public function set( $key, $value, $exptime = 0, $flags = 0 ) {
$ok = $this->setMulti( [ $key => $value ], $exptime );
- if ( ( $flags & self::WRITE_SYNC ) == self::WRITE_SYNC ) {
- $ok = $this->waitForReplication() && $ok;
- }
return $ok;
}
- protected function cas( $casToken, $key, $value, $exptime = 0 ) {
+ public function add( $key, $value, $exptime = 0, $flags = 0 ) {
+ $added = $this->insertMulti( [ $key => $value ], $exptime, $flags, false );
+
+ return $added;
+ }
+
+ protected function cas( $casToken, $key, $value, $exptime = 0, $flags = 0 ) {
list( $serverIndex, $tableName ) = $this->getTableByKey( $key );
$db = null;
$silenceScope = $this->silenceTransactionProfiler();
return (bool)$db->affectedRows();
}
- public function delete( $key, $flags = 0 ) {
- $ok = true;
+ public function deleteMulti( array $keys, $flags = 0 ) {
+ $keysByTable = [];
+ foreach ( $keys as $key ) {
+ list( $serverIndex, $tableName ) = $this->getTableByKey( $key );
+ $keysByTable[$serverIndex][$tableName][] = $key;
+ }
- list( $serverIndex, $tableName ) = $this->getTableByKey( $key );
- $db = null;
+ $result = true;
$silenceScope = $this->silenceTransactionProfiler();
- try {
- $db = $this->getDB( $serverIndex );
- $db->delete(
- $tableName,
- [ 'keyname' => $key ],
- __METHOD__ );
- } catch ( DBError $e ) {
- $this->handleWriteError( $e, $db, $serverIndex );
- $ok = false;
+ foreach ( $keysByTable as $serverIndex => $serverKeys ) {
+ $db = null;
+ try {
+ $db = $this->getDB( $serverIndex );
+ } catch ( DBError $e ) {
+ $this->handleWriteError( $e, $db, $serverIndex );
+ $result = false;
+ continue;
+ }
+
+ foreach ( $serverKeys as $tableName => $tableKeys ) {
+ try {
+ $db->delete( $tableName, [ 'keyname' => $tableKeys ], __METHOD__ );
+ } catch ( DBError $e ) {
+ $this->handleWriteError( $e, $db, $serverIndex );
+ $result = false;
+ }
+
+ }
}
+
if ( ( $flags & self::WRITE_SYNC ) == self::WRITE_SYNC ) {
- $ok = $this->waitForReplication() && $ok;
+ $result = $this->waitForReplication() && $result;
}
+ return $result;
+ }
+
+ public function delete( $key, $flags = 0 ) {
+ $ok = $this->deleteMulti( [ $key ], $flags );
+
return $ok;
}
[ 'value', 'exptime' ],
[ 'keyname' => $key ],
__METHOD__,
- [ 'FOR UPDATE' ] );
+ [ 'FOR UPDATE' ]
+ );
if ( $row === false ) {
// Missing
-
- return null;
+ return false;
}
$db->delete( $tableName, [ 'keyname' => $key ], __METHOD__ );
if ( $this->isExpired( $db, $row->exptime ) ) {
// Expired, do not reinsert
-
- return null;
+ return false;
}
$oldValue = intval( $this->unserialize( $db->decodeBlob( $row->value ) ) );
$newValue = $oldValue + $step;
- $db->insert( $tableName,
+ $db->insert(
+ $tableName,
[
'keyname' => $key,
'value' => $db->encodeBlob( $this->serialize( $newValue ) ),
'exptime' => $row->exptime
- ], __METHOD__, 'IGNORE' );
+ ],
+ __METHOD__,
+ 'IGNORE'
+ );
if ( $db->affectedRows() == 0 ) {
// Race condition. See T30611
- $newValue = null;
+ $newValue = false;
}
} catch ( DBError $e ) {
$this->handleWriteError( $e, $db, $serverIndex );
}
public function merge( $key, callable $callback, $exptime = 0, $attempts = 10, $flags = 0 ) {
- $ok = $this->mergeViaCas( $key, $callback, $exptime, $attempts );
+ $ok = $this->mergeViaCas( $key, $callback, $exptime, $attempts, $flags );
if ( ( $flags & self::WRITE_SYNC ) == self::WRITE_SYNC ) {
$ok = $this->waitForReplication() && $ok;
}
return $ok;
}
- public function changeTTL( $key, $expiry = 0 ) {
+ public function changeTTL( $key, $expiry = 0, $flags = 0 ) {
list( $serverIndex, $tableName ) = $this->getTableByKey( $key );
$db = null;
$silenceScope = $this->silenceTransactionProfiler();
}
// Determine the options which affect this article
- $casToken = null;
$optionsKey = $this->mMemc->get(
- $this->getOptionsKey( $article ), $casToken, BagOStuff::READ_VERIFIED );
+ $this->getOptionsKey( $article ), BagOStuff::READ_VERIFIED );
if ( $optionsKey instanceof CacheTime ) {
if ( $useOutdated < self::USE_EXPIRED && $optionsKey->expired( $article->getTouched() ) ) {
$this->incrementStats( $article, "miss.expired" );
$casToken = null;
/** @var ParserOutput $value */
- $value = $this->mMemc->get( $parserOutputKey, $casToken, BagOStuff::READ_VERIFIED );
+ $value = $this->mMemc->get( $parserOutputKey, BagOStuff::READ_VERIFIED );
if ( !$value ) {
wfDebug( "ParserOutput cache miss.\n" );
$this->incrementStats( $article, "miss.absent" );
}
}
- // Kept BC for now, remove when possible
public function profileIn( $functionname ) {
+ wfDeprecated( __METHOD__, '1.33' );
}
public function profileOut( $functionname ) {
+ wfDeprecated( __METHOD__, '1.33' );
}
/**
}
/**
- * Log the data to some store or even the page output
+ * Log the data to the backing store for all ProfilerOutput instances that have one
*
* @since 1.25
*/
return;
}
- $outputs = $this->getOutputs();
- if ( !$outputs ) {
- return;
+ $outputs = [];
+ foreach ( $this->getOutputs() as $output ) {
+ if ( !$output->logsToOutput() ) {
+ $outputs[] = $output;
+ }
}
- $stats = $this->getFunctionStats();
- foreach ( $outputs as $output ) {
- $output->log( $stats );
+ if ( $outputs ) {
+ $stats = $this->getFunctionStats();
+ foreach ( $outputs as $output ) {
+ $output->log( $stats );
+ }
}
}
/**
- * Output current data to the page output if configured to do so
+ * Log the data to the script/request output for all ProfilerOutput instances that do so
*
* @throws MWException
* @since 1.26
*/
public function logDataPageOutputOnly() {
+ $outputs = [];
foreach ( $this->getOutputs() as $output ) {
- if ( $output instanceof ProfilerOutputText ) {
- $stats = $this->getFunctionStats();
+ if ( $output->logsToOutput() ) {
+ $outputs[] = $output;
+ }
+ }
+
+ if ( $outputs ) {
+ $stats = $this->getFunctionStats();
+ foreach ( $outputs as $output ) {
$output->log( $stats );
}
}
return true;
}
+ /**
+ * Does log() just send the data to the request/script output?
+ * @return bool
+ * @since 1.33
+ */
+ public function logsToOutput() {
+ return false;
+ }
+
/**
* Log MediaWiki-style profiling data
*
parent::__construct( $collector, $params );
$this->thresholdMs = $params['thresholdMs'] ?? 1.0;
}
+
+ public function logsToOutput() {
+ return true;
+ }
+
public function log( array $stats ) {
if ( $this->collector->getTemplated() ) {
$out = '';
*
* @since 1.21
*
- * @param string $languageCode
+ * @param string|null $languageCode
*/
public function setLanguageCode( $languageCode ) {
- if ( !Language::isValidCode( $languageCode ) ) {
+ if ( $languageCode !== null && !Language::isValidCode( $languageCode ) ) {
throw new InvalidArgumentException( "$languageCode is not a valid language code." );
}
$this->languageCode = $languageCode;
'mediawiki.special',
'mediawiki.special.changeslist',
] );
+ $out->addModules( 'mediawiki.special.recentchanges' );
$this->addHelpLink( 'Help:User contributions' );
$this->opts = [];
$labelNewbies . '<br>' . $labelUsername . ' ' . $input . ' '
);
+ $hidden = $this->opts['namespace'] === '' ? ' mw-input-hidden' : '';
$namespaceSelection = Xml::tags(
'div',
[],
) . "\u{00A0}" .
Html::rawElement(
'span',
- [ 'class' => 'mw-input-with-label' ],
+ [ 'class' => 'mw-input-with-label' . $hidden ],
Xml::checkLabel(
$this->msg( 'invert' )->text(),
'nsInvert',
- 'nsInvert',
+ 'nsinvert',
$this->opts['nsInvert'],
[
'title' => $this->msg( 'tooltip-invert' )->text(),
]
) . "\u{00A0}"
) .
- Html::rawElement( 'span', [ 'class' => 'mw-input-with-label' ],
+ Html::rawElement( 'span', [ 'class' => 'mw-input-with-label' . $hidden ],
Xml::checkLabel(
$this->msg( 'namespace_association' )->text(),
'associated',
- 'associated',
+ 'nsassociated',
$this->opts['associated'],
[
'title' => $this->msg( 'tooltip-namespace_association' )->text(),
$this->addHelpLink( 'Help:Watching pages' );
$output->addModuleStyles( [ 'mediawiki.special' ] );
$output->addModules( [
+ 'mediawiki.special.recentchanges',
'mediawiki.special.watchlist',
] );
'class' => 'namespaceselector',
]
) . "\n";
- $namespaceForm .= '<span class="mw-input-with-label">' . Xml::checkLabel(
+ $hidden = $opts['namespace'] === '' ? ' mw-input-hidden' : '';
+ $namespaceForm .= '<span class="mw-input-with-label' . $hidden . '">' . Xml::checkLabel(
$this->msg( 'invert' )->text(),
'invert',
'nsinvert',
$opts['invert'],
[ 'title' => $this->msg( 'tooltip-invert' )->text() ]
) . "</span>\n";
- $namespaceForm .= '<span class="mw-input-with-label">' . Xml::checkLabel(
+ $namespaceForm .= '<span class="mw-input-with-label' . $hidden . '">' . Xml::checkLabel(
$this->msg( 'namespace_association' )->text(),
'associated',
'nsassociated',
Hooks::run( 'UserSaveSettings', [ $this ] );
$this->clearSharedCache();
- $this->getUserPage()->invalidateCache();
+ $this->getUserPage()->purgeSquid();
}
/**
*/
public static function isValidCode( $code ) {
static $cache = [];
+ Assert::parameterType( 'string', $code, '$code' );
if ( !isset( $cache[$code] ) ) {
// People think language codes are html safe, so enforce it.
// Ideally we should only allow a-zA-Z0-9-
"markedaspatrollederror": "Yoxlanmadı",
"markedaspatrollederror-noautopatrol": "Öz dəyişikliklərinizi yoxlayıb işarələyə bilməzsiniz.",
"markedaspatrollednotify": "\"$1\" səhifəsindəki bu redaktə patrullanmış kimi işarələndi.",
+ "markedaspatrollederrornotify": "Patrullanma uğursuz oldu.",
"patrol-log-page": "Patrul gündəliyi",
"patrol-log-header": "Bu yoxlanmış dəyişikliklərin gündəliyidir.",
"deletedrevision": "Köhnə versiyaları silindi $1.",
"logentry-suppress-block": "$1 $3ی بۆ ماوەی $5 $6 بەربەست کرد",
"logentry-suppress-reblock": "$1 ھەڵبژاردەکانی بەربەستنی $3ی گۆڕی بە ماوەی بەسەرچوونی $5 $6",
"logentry-import-upload": "$1 {{GENDER:$2|بارکرد}} $3 بە بەکارھێنانی [[special:Import|بارکەر]]",
+ "logentry-import-interwiki-details": "$1 $3ی لە $5ەوە ھەناردە کرد ($4 بەسەرداچوونەوە)",
"logentry-move-move": "$1 پەڕەی $3ی {{GENDER:$2|گواستەوە}} بۆ $4",
"logentry-move-move-noredirect": "$1 پەڕەی $3ی بە بێ بەجێھشتنی ڕەوانەکەرێک {{GENDER:$2|گواستەوە}} بۆ $4",
"logentry-move-move_redir": "$1 پەڕەی $3 {{GENDER:$2|گواستەوە}} بۆ $4 کە پێشتر ڕەوانەکەر بوو",
"tog-norollbackdiff": "Unterschiede nach dem Zurücksetzen nicht anzeigen",
"tog-useeditwarning": "Warnen, sofern eine zur Bearbeitung geöffnete Seite verlassen wird, die nicht gespeicherte Änderungen enthält",
"tog-prefershttps": "Immer eine sichere Verbindung benutzen, solange ich angemeldet bin",
+ "tog-showrollbackconfirmation": "Beim Klicken auf einen Zurücksetzen-Link eine Bestätigungsaufforderung anzeigen",
"underline-always": "immer",
"underline-never": "nie",
"underline-default": "abhängig von der Benutzeroberfläche oder Browsereinstellung",
"Pipino-pumuki",
"Carlosmg.dg",
"Mynor Archila",
- "Jorge Ubilla"
+ "Jorge Ubilla",
+ "Marcelo9987"
]
},
"tog-underline": "Enlaces a subrayar:",
"ipb-sitewide": "En todo el sitio",
"ipb-partial": "Parcial",
"ipb-sitewide-help": "Todas las páginas en la Wiki y todas las acciones de contribución.",
+ "ipb-partial-help": "Páginas concretas o nombres.",
"ipb-pages-label": "Páginas",
"ipb-namespaces-label": "Espacios de nombres",
"badipaddress": "La dirección IP no tiene el formato correcto.",
"passwordpolicies-policy-maximalpasswordlength": "La contraseña no puede tener más de $1 {{PLURAL:$1|caracter|caracteres}}",
"passwordpolicies-policy-passwordcannotbepopular": "La contraseña no puede {{PLURAL:$1|ser la contraseña más popular|encontrarse en la lista de $1 contraseñas populares}}",
"passwordpolicies-policy-passwordnotinlargeblacklist": "La contraseña no puede estar en la lista de las 100.000 contraseñas más usadas.",
+ "passwordpolicies-policyflag-forcechange": "Sugerir cambio al iniciar sesion",
+ "passwordpolicies-policyflag-suggestchangeonlogin": "Sugerir cambio al iniciar sesion",
"easydeflate-invaliddeflate": "El contenido proporcionado no esta comprimido correctamente",
"unprotected-js": "Por razones de seguridad, JavaScript no se puede cargar desde páginas desprotegidas. Crea javascript solo en MediaWiki: espacio de nombres o como subpágina de usuario"
}
"feed-rss": "RSS",
"red-link-title": "$1 (de side bestiet net)",
"nstab-main": "Side",
- "nstab-user": "Meidogger",
+ "nstab-user": "Meidoggerside",
"nstab-media": "Mediaside",
"nstab-special": "Bysûndere side",
"nstab-project": "Projektside",
"mainpage-nstab": "Haadside",
"nosuchaction": "Unbekende aksje.",
"nosuchactiontext": "De opdracht yn de URL is ûnjildich.\nMooglik hasto in typefout makke yn de URL of in ferkearde keppeling folge.\nIt soe likegoed in programmatuerflater fan {{SITENAME}} wêze kinne.",
- "nosuchspecialpage": "Unbekende side",
- "nospecialpagetext": "<strong>Jo hawwe in Wiki-side opfrege dy't net bekend is by it Wiki-programma.</strong>",
+ "nosuchspecialpage": "Gjin soksoarte bysûndere side",
+ "nospecialpagetext": "<strong>Jo hawwe in ûnjildige bysûndere side opfrege.</strong>\n\nIn list fan jildige bysûndere siden stiet op [[Special:SpecialPages|{{int:specialpages}}]].",
"error": "Flater",
"databaseerror": "Databankfout",
"databaseerror-query": "Sykopdracht: $1",
"logouttext": "<strong>Jo binne no ôfmeld.</strong>\n\nGuon siden kinne noch foar it ljocht komme, krekt as wiesto noch oanmeld. Asto de cache fan dyn webblêder leechhellest feroaret dat wer.",
"welcomeuser": "Wolkom, $1!",
"yourname": "Brûkersnamme:",
- "userlogin-yourname": "Brûkersnamme",
- "userlogin-yourname-ph": "Jou jo brûkersnamme",
+ "userlogin-yourname": "Meidochnamme",
+ "userlogin-yourname-ph": "Jou jo meidochnamme",
"createacct-another-username-ph": "Jou jo brûkersnamme",
"yourpassword": "Wachtwurd:",
"userlogin-yourpassword": "Wachtwurd",
"nocookiesnew": "De brûker is oanmakke mar net oanmeld. {{SITENAME}} brûkt cookies foar it oanmelden fan brûkers. Skeakelje dy yn en meld jo dan oan mei jo nije brûkersnamme en wachtwurd.",
"nocookieslogin": "{{SITENAME}} brûkt cookies foar it oanmelden fan brûkers. Jo hawwe cookies útskeakele. Skeakelje dy opsje oan en besykje it nochris.",
"nocookiesforlogin": "{{int:nocookieslogin}}",
- "noname": "Jo moatte in meidognamme opjaan.",
+ "noname": "Jo hawwe gjin jildige meidochnamme opjûn.",
"loginsuccesstitle": "Oanmelden slagge.",
"loginsuccess": "<strong>Jo binne no oanmeld op {{SITENAME}} as \"$1\".</strong>",
"nosuchuser": "Der is gjin meidogger \"$1\".\nKontrolearje de stavering, of [[Special:CreateAccount|meitsje in nije meidogger oan]].",
"action-edit": "dizze side te bewurkjen",
"action-createpage": "siden oan te meitsjen",
"action-createtalk": "oerlissiden oan te meitsjen",
- "action-createaccount": "dizze meidogger oan te meitsjen",
+ "action-createaccount": "oanmeitsjen fan dit meidochakkount",
"action-minoredit": "dizze bewurking as lyts te markearjen",
"action-move": "dizze side in oare namme te jaan",
"action-move-subpages": "dizze side en de derby hearrende subsiden in oare namme te jaan",
"rcshowhidebots": "bots $1",
"rcshowhidebots-show": "werjaan",
"rcshowhidebots-hide": "ferbergje",
- "rcshowhideliu": "registrearre brûkers $1",
+ "rcshowhideliu": "Registrearre meidoggers $1",
"rcshowhideliu-show": "werjaan",
"rcshowhideliu-hide": "ferbergje",
"rcshowhideanons": "$1 anonimen",
"protectedpages-unknown-timestamp": "Unbekend",
"protectedtitles": "Skoattele titels",
"protectedtitlesempty": "Der binne op it stuit gjin sidenammen befeilige, dy't oan dizze betingsten foldogge.",
- "listusers": "Meidoggerlist",
+ "listusers": "Meidoggerslist",
"listusers-editsonly": "Allinne brûkers mei bewurkings werjaan",
"listusers-creationsort": "Oarderje op dei fan oanmeitsjen",
"usereditcount": "$1 {{PLURAL:$1|bewurking|bewurkings}}",
"booksources-search": "Sykje",
"booksources-text": "Hjirûnder is in list mei keppelings nei oare websites dy't nije of brûkte boeken ferkeapje en dy't faaks mear ynformaasje hawwe oer it boek dat jo sykje:",
"booksources-invalid-isbn": "It ynjûne ISBN liket net jildich te wêzen.\nKontrolearje oft jo faaks in flater makke hawwe by de ynfier.",
- "specialloguserlabel": "Útfierende meidogger:",
- "speciallogtitlelabel": "Doel (titel of brûker):",
+ "specialloguserlabel": "Utfierder:",
+ "speciallogtitlelabel": "Doel (sidetitel of {{ns:user}}:meidochnamme foar meidogger):",
"log": "Lochs",
"all-logs-page": "Alle iepenbiere lochboeken",
- "alllogstext": "Dit is it kombinearre logboek fan {{SITENAME}}.\nJo kinne ek kieze foar spesifike logboeken en filterje op brûker (haadstêfgefoelich) en sidenamme (haadstêfgefoelich).",
+ "alllogstext": "Gearfoege werjefte fan alle beskikbere lochs op {{SITENAME}}.\nJo kinne it byld beheine troch it kiezen fan in lochtype, de meidochnamme (haadlettergefoelich) of de oanbelangjende side (ek haadlettergefoelich).",
"logempty": "Gjin treffers yn it loch.",
"log-title-wildcard": "Siden sykje dy't mei dizze namme begjinne",
"allpages": "Alle siden",
"protect-locked-dblock": "It befeiligingsnivo kin net feroare wurde om't de database sletten is.\nHjir binne de hjoeddeiske ynstellingen foar de side '''$1''':",
"protect-locked-access": "'''Jo brûker hat gjin rjochten om it befeiligingsnivo te feroarjen.'''\nDit binne de rinnende ynstellingen foar de side '''$1''':",
"protect-cascadeon": "Dizze side is op 't stuit befeilige, om't er yn 'e folgjende {{PLURAL:$1|side|siden}} opnommen is, dy't befeilige {{PLURAL:$1|is|binne}} mei de kaskade-opsje. It befeiligingsnivo feroarje hat alhiel gjin effekt.",
- "protect-default": "Tastean foar alle brûkers",
+ "protect-default": "Tastean foar alle meidoggers",
"protect-fallback": "Hjir is it rjocht \"$1\" foar nedich",
"protect-level-autoconfirmed": "Slút anonymen út",
"protect-level-sysop": "Allinnich behearders",
"namespace": "Nammeromte:",
"invert": "Seleksje útsein",
"blanknamespace": "(Haad)",
- "contributions": "{{GENDER:$1|Meidogger}}-bydragen",
+ "contributions": "Bydragen fan 'e {{GENDER:$1|meidogger|meidochster}}",
"contributions-title": "Bydragen fan $1",
"mycontris": "Bydragen",
"anoncontribs": "Bydragen",
"uctop": "lêste feroaring",
"month": "Fan moanne (en earder):",
"year": "Fan jier (en earder):",
- "sp-contributions-newbies": "Allinne bydragen fan nije brûkers besjen",
+ "sp-contributions-newbies": "Allinne bydragen fan nije akkounts besjen",
"sp-contributions-newbies-sub": "Foar nijlingen",
"sp-contributions-newbies-title": "Bydragen fan nije meidoggers",
"sp-contributions-blocklog": "Blokkearlochboek",
"sp-contributions-talk": "oerlis",
"sp-contributions-userrights": "behear fan meidoggerrjochten",
"sp-contributions-search": "Sykje nei bydragen",
- "sp-contributions-username": "IP Adres of meidoggernamme:",
+ "sp-contributions-username": "IP-adres of meidochnamme:",
"sp-contributions-submit": "Sykje",
"whatlinkshere": "Wat is hjirmei keppele?",
"whatlinkshere-title": "Siden dy't keppele binne mei \"$1\"",
"importlogpage": "Ymportlochboek",
"import-logentry-upload-detail": "$1 {{PLURAL:$1|ferzje|ferzjes}}",
"import-logentry-interwiki-detail": "$1 {{PLURAL:$1|ferzje|ferzjes}} fan $2",
- "tooltip-pt-userpage": "Myn brûkersside",
+ "tooltip-pt-userpage": "Jo {{GENDER:|meidogger}}side",
"tooltip-pt-mytalk": "Jo oerlisside",
"tooltip-pt-preferences": "Myn foarkarynstellings",
"tooltip-pt-watchlist": "List fan siden dy'sto besjochst op feroarings",
"tooltip-t-recentchangeslinked": "De lêste feroarings yn siden dêr't dizze side nei ferwiisd",
"tooltip-feed-rss": "RSS-feed foar dizze side",
"tooltip-feed-atom": "Atom-feed foar dizze side",
- "tooltip-t-contributions": "Bydragen fan dizze brûker",
- "tooltip-t-emailuser": "Stjoer in e-mail nei {{GENDER:$1|dizze meidogger}}",
+ "tooltip-t-contributions": "List fan bydragen troch dizze {{GENDER:$1|meidogger|meidochster}}",
+ "tooltip-t-emailuser": "Stjoer in e-mail nei dizze {{GENDER:$1|meidogger|meidochster}}",
"tooltip-t-upload": "Bestannen oplade",
- "tooltip-t-specialpages": "List fan alle spesjale siden",
+ "tooltip-t-specialpages": "List fan alle bysûndere siden",
"tooltip-t-print": "Ofdrukferzje fan dizze side",
"tooltip-t-permalink": "Bliuwende keppeling nei dizze ferzje fan 'e side",
"tooltip-ca-nstab-main": "Ynhâldlike side sjen litte",
- "tooltip-ca-nstab-user": "Brûkersside sjen litte",
- "tooltip-ca-nstab-special": "Dit is in spesjale side, dy't net bewurke wurde kin",
+ "tooltip-ca-nstab-user": "Besjoch de meidoggerside",
+ "tooltip-ca-nstab-special": "Dit is in bysûndere side, en kin net bewurke wurde",
"tooltip-ca-nstab-project": "Projektside sjen litte",
"tooltip-ca-nstab-image": "De bestânsside sjen litte",
"tooltip-ca-nstab-mediawiki": "Systeemberjocht sjen litte",
"passwordpolicies-policy-maximalpasswordlength": "הסיסמה חייבת להיות קצרה יותר {{PLURAL:$1|מתו אחד|מ־$1 תווים}}",
"passwordpolicies-policy-passwordcannotbepopular": "הסיסמה לא יכולה להיות זהה {{PLURAL:$1|לסיסמה נפוצה|לאחת הסיסמאות שנמצאות ברשימה של $1 הסיסמאות הנפוצות}}",
"passwordpolicies-policy-passwordnotinlargeblacklist": "הסיסמה לא יכולה להיות ברשימת 100,000 הסיסמאות הנפוצות ביותר.",
- "passwordpolicies-policyflag-forcechange": "נדרש שינוי הסיסמה בכניסה",
+ "passwordpolicies-policyflag-forcechange": "לדרוש שינוי בעת כניסה לחשבון",
+ "passwordpolicies-policyflag-suggestchangeonlogin": "להציע שינוי בעת כניסה לחשבון",
"easydeflate-invaliddeflate": "התוכן שהועבר אינו דחוס כנדרש",
"unprotected-js": "מסיבות אבטחה, לא ניתן לטעון JavaScript מדפים שאינם מוגנים. ניתן ליצור סקריפטי JavaScript רק במרחב השם \"מדיה ויקי:\" או בדפי משנה של דף המשתמש."
}
$this->ensureMockDatabaseConnection( $db );
$oldOverrides = $oldOverrides + self::$schemaOverrideDefaults;
- $originalTables = $this->listOriginalTables( $db, 'unprefixed' );
+ $originalTables = $this->listOriginalTables( $db );
// Drop tables that need to be restored or removed.
$tablesToDrop = array_merge( $oldOverrides['create'], $oldOverrides['alter'] );
$this->ensureMockDatabaseConnection( $db );
// Drop the tables that will be created by the schema scripts.
- $originalTables = $this->listOriginalTables( $db, 'unprefixed' );
+ $originalTables = $this->listOriginalTables( $db );
$tablesToDrop = array_intersect( $originalTables, $overrides['create'] );
if ( $tablesToDrop ) {
}
/**
- * Lists all tables in the live database schema.
+ * Lists all tables in the live database schema, without a prefix.
*
* @param IMaintainableDatabase $db
- * @param string $prefix Either 'prefixed' or 'unprefixed'
* @return array
*/
- private function listOriginalTables( IMaintainableDatabase $db, $prefix = 'prefixed' ) {
+ private function listOriginalTables( IMaintainableDatabase $db ) {
if ( !isset( $db->_originalTablePrefix ) ) {
throw new LogicException( 'No original table prefix know, cannot list tables!' );
}
$originalTables = $db->listTables( $db->_originalTablePrefix, __METHOD__ );
- if ( $prefix === 'unprefixed' ) {
- $originalPrefixRegex = '/^' . preg_quote( $db->_originalTablePrefix, '/' ) . '/';
- $originalTables = array_map(
- function ( $pt ) use ( $originalPrefixRegex ) {
- return preg_replace( $originalPrefixRegex, '', $pt );
- },
- $originalTables
- );
- }
- return $originalTables;
+ $unittestPrefixRegex = '/^' . preg_quote( $this->dbPrefix(), '/' ) . '/';
+ $originalPrefixRegex = '/^' . preg_quote( $db->_originalTablePrefix, '/' ) . '/';
+
+ $originalTables = array_filter(
+ $originalTables,
+ function ( $pt ) use ( $unittestPrefixRegex ) {
+ return !preg_match( $unittestPrefixRegex, $pt );
+ }
+ );
+
+ $originalTables = array_map(
+ function ( $pt ) use ( $originalPrefixRegex ) {
+ return preg_replace( $originalPrefixRegex, '', $pt );
+ },
+ $originalTables
+ );
+
+ return array_unique( $originalTables );
}
/**
throw new LogicException( 'No original table prefix know, cannot restore tables!' );
}
- $originalTables = $this->listOriginalTables( $db, 'unprefixed' );
+ $originalTables = $this->listOriginalTables( $db );
$tables = array_intersect( $tables, $originalTables );
$dbClone = new CloneDatabase( $db, $tables, $db->tablePrefix(), $db->_originalTablePrefix );
* @param IDatabase $target
*/
public function copyTestData( IDatabase $source, IDatabase $target ) {
- $tables = self::listOriginalTables( $source, 'unprefixed' );
+ if ( $this->db->getType() === 'sqlite' ) {
+ // SQLite uses a non-temporary copy of the searchindex table for testing,
+ // which gets deleted and re-created when setting up the secondary connection,
+ // causing "Error 17" when trying to copy the data. See T191863#4130112.
+ throw new RuntimeException(
+ 'Setting up a secondary database connection with test data is currently not'
+ . 'with SQLite. You may want to use markTestSkippedIfDbType() to bypass this issue.'
+ );
+ }
+
+ $tables = self::listOriginalTables( $source );
foreach ( $tables as $table ) {
$res = $source->select( $table, '*', [], __METHOD__ );
'ctd_user_defined' => 1
],
];
- $res = $dbr->select( 'change_tag_def', [ 'ctd_name', 'ctd_user_defined' ], '' );
+ $res = $dbr->select(
+ 'change_tag_def',
+ [ 'ctd_name', 'ctd_user_defined' ],
+ '',
+ __METHOD__,
+ [ 'ORDER BY' => 'ctd_name' ]
+ );
$this->assertEquals( $expected, iterator_to_array( $res, false ) );
}
}
public function __construct( $testName, array $opts = [] ) {
$this->testName = $testName;
- $this->profiler = new ProfilerStub( [] );
+ $this->profiler = null;
$this->trxProfiler = new TransactionProfiler();
$this->cliMode = $opts['cliMode'] ?? true;
$this->connLogger = new \Psr\Log\NullLogger();
// Handle some internal calls from the Database class
$check = $fname;
- if ( preg_match( '/^Wikimedia\\\\Rdbms\\\\Database::query \((.+)\)$/', $fname, $m ) ) {
+ if ( preg_match(
+ '/^Wikimedia\\\\Rdbms\\\\Database::(?:query|beginIfImplied) \((.+)\)$/',
+ $fname,
+ $m
+ ) ) {
$check = $m[1];
}
}
$this->cache->delete( $this->cache->makeKey( self::TEST_KEY ) );
+ $this->cache->delete( $this->cache->makeKey( self::TEST_KEY ) . ':lock' );
}
/**
* @covers BagOStuff::mergeViaCas
*/
public function testMerge() {
- $calls = 0;
$key = $this->cache->makeKey( self::TEST_KEY );
- $callback = function ( BagOStuff $cache, $key, $oldVal ) use ( &$calls ) {
+ $locks = false;
+ $checkLockingCallback = function ( BagOStuff $cache, $key, $oldVal ) use ( &$locks ) {
+ $locks = $cache->get( "$key:lock" );
+
+ return false;
+ };
+
+ $this->cache->merge( $key, $checkLockingCallback, 5 );
+ $this->assertFalse( $this->cache->get( $key ) );
+
+ $calls = 0;
+ $casRace = false; // emulate a race
+ $callback = function ( BagOStuff $cache, $key, $oldVal ) use ( &$calls, &$casRace ) {
++$calls;
+ if ( $casRace ) {
+ // Uses CAS instead?
+ $cache->set( $key, 'conflict', 5 );
+ }
return ( $oldVal === false ) ? 'merged' : $oldVal . 'merged';
};
$this->assertEquals( 'mergedmerged', $this->cache->get( $key ) );
$calls = 0;
- $this->cache->lock( $key );
- $this->assertFalse( $this->cache->merge( $key, $callback, 1 ), 'Non-blocking merge' );
- $this->cache->unlock( $key );
- $this->assertEquals( 0, $calls );
+ if ( $locks ) {
+ // merge were something else already was merging (e.g. had the lock)
+ $this->cache->lock( $key );
+ $this->assertFalse(
+ $this->cache->merge( $key, $callback, 5, 1 ),
+ 'Non-blocking merge (locking)'
+ );
+ $this->cache->unlock( $key );
+ $this->assertEquals( 0, $calls );
+ } else {
+ $casRace = true;
+ $this->assertFalse(
+ $this->cache->merge( $key, $callback, 5, 1 ),
+ 'Non-blocking merge (CAS)'
+ );
+ $this->assertEquals( 1, $calls );
+ }
}
/**
* @covers BagOStuff::merge
* @covers BagOStuff::mergeViaLock
+ * @dataProvider provideTestMerge_fork
*/
- public function testMerge_fork() {
+ public function testMerge_fork( $exists, $winsLocking, $resLocking, $resCAS ) {
$key = $this->cache->makeKey( self::TEST_KEY );
- $callback = function ( BagOStuff $cache, $key, $oldVal ) {
- return ( $oldVal === false ) ? 'merged' : $oldVal . 'merged';
+ $pCallback = function ( BagOStuff $cache, $key, $oldVal ) {
+ return ( $oldVal === false ) ? 'init-parent' : $oldVal . '-merged-parent';
+ };
+ $cCallback = function ( BagOStuff $cache, $key, $oldVal ) {
+ return ( $oldVal === false ) ? 'init-child' : $oldVal . '-merged-child';
};
+
+ if ( $exists ) {
+ $this->cache->set( $key, 'x', 5 );
+ }
+
/*
* Test concurrent merges by forking this process, if:
* - not manually called with --use-bagostuff
$fork &= !$this->cache instanceof MultiWriteBagOStuff;
if ( $fork ) {
$pid = null;
+ $locked = false;
// Function to start merge(), run another merge() midway through, then finish
- $outerFunc = function ( BagOStuff $cache, $key, $oldVal ) use ( $callback, &$pid ) {
+ $func = function ( BagOStuff $cache, $key, $cur )
+ use ( $pCallback, $cCallback, &$pid, &$locked )
+ {
$pid = pcntl_fork();
if ( $pid == -1 ) {
return false;
} elseif ( $pid ) {
+ $locked = $cache->get( "$key:lock" ); // parent has lock?
pcntl_wait( $status );
- return $callback( $cache, $key, $oldVal );
+ return $pCallback( $cache, $key, $cur );
} else {
- $this->cache->merge( $key, $callback, 0, 1 );
+ $this->cache->merge( $key, $cCallback, 0, 1 );
// Bail out of the outer merge() in the child process since it does not
// need to attempt to write anything. Success is checked by the parent.
parent::tearDown(); // avoid phpunit notices
};
// attempt a merge - this should fail
- $merged = $this->cache->merge( $key, $outerFunc, 0, 1 );
+ $merged = $this->cache->merge( $key, $func, 0, 1 );
if ( $pid == -1 ) {
return; // can't fork, ignore this test...
}
- // merge has failed because child process was merging (and we only attempted once)
- $this->assertFalse( $merged );
-
- // make sure the child's merge is completed and verify
- $this->assertEquals( $this->cache->get( $key ), 'mergedmerged' );
+ if ( $locked ) {
+ // merge succeed since child was locked out
+ $this->assertEquals( $winsLocking, $merged );
+ $this->assertEquals( $this->cache->get( $key ), $resLocking );
+ } else {
+ // merge has failed because child process was merging (and we only attempted once)
+ $this->assertEquals( !$winsLocking, $merged );
+ $this->assertEquals( $this->cache->get( $key ), $resCAS );
+ }
} else {
$this->markTestSkipped( 'No pcntl methods available' );
}
}
+ function provideTestMerge_fork() {
+ return [
+ // (already exists, parent wins if locking, result if locking, result if CAS)
+ [ false, true, 'init-parent', 'init-child' ],
+ [ true, true, 'x-merged-parent', 'x-merged-child' ]
+ ];
+ }
+
/**
* @covers BagOStuff::changeTTL
*/
$this->cache->delete( $key4 );
}
+ /**
+ * @covers BagOStuff::setMulti
+ * @covers BagOStuff::deleteMulti
+ */
+ public function testSetDeleteMulti() {
+ $map = [
+ $this->cache->makeKey( 'test-1' ) => 'Siberian',
+ $this->cache->makeKey( 'test-2' ) => [ 'Huskies' ],
+ $this->cache->makeKey( 'test-3' ) => [ 'are' => 'the' ],
+ $this->cache->makeKey( 'test-4' ) => (object)[ 'greatest' => 'animal' ],
+ $this->cache->makeKey( 'test-5' ) => 4,
+ $this->cache->makeKey( 'test-6' ) => 'ever'
+ ];
+
+ $this->cache->setMulti( $map, 5 );
+ $this->assertEquals(
+ $map,
+ $this->cache->getMulti( array_keys( $map ) )
+ );
+
+ $this->assertTrue( $this->cache->deleteMulti( array_keys( $map ), 5 ) );
+
+ $this->assertEquals(
+ [],
+ $this->cache->getMulti( array_keys( $map ) )
+ );
+ }
+
/**
* @covers BagOStuff::getScopedLock
*/
}
public function testGetImageSize() {
- $this->assertArrayEquals(
+ $this->assertSame(
[ 2480, 3508, 'DjVu', 'width="2480" height="3508"' ],
$this->handler->getImageSize( null, $this->filePath . '/LoremIpsum.djvu' ),
'Test file LoremIpsum.djvu should have a size of 2480 * 3508'
public function testGetPageDimensions() {
$file = $this->dataFile( 'LoremIpsum.djvu', 'image/x.djvu' );
- $this->assertArrayEquals(
- [ 2480, 3508 ],
+ $this->assertSame(
+ [ 'width' => 2480, 'height' => 3508 ],
$this->handler->getPageDimensions( $file, 1 ),
'Page 1 of test file LoremIpsum.djvu should have a size of 2480 * 3508'
);
'Learner1', 'Learner2', 'Learner3', 'Learner4',
'Experienced1',
],
- $this->fetchUsers( [ 'learner', 'experienced' ], $now ),
- 'Learner and more experienced'
+ $this->fetchUsers( [ 'learner', 'experienced' ], $now )
);
}
$this->assertFileContains( $testFileName, $sparql );
$processed = $processedProperty->getValue( $dumpScript );
- $expectedProcessed = $preProcessed;
+ $expectedProcessed = array_keys( $preProcessed );
foreach ( $result as $row ) {
if ( isset( $row->_processed ) ) {
$this->assertArrayHasKey( $row->_processed, $processed,
$expectedProcessed[] = $row->_processed;
}
}
- $this->assertArrayEquals( $expectedProcessed, array_keys( $processed ),
+ $this->assertSame( $expectedProcessed, array_keys( $processed ),
'Processed array has wrong items' );
}
$css = file_get_contents( $basepath . 'comments.css' );
$files = CSSMin::getLocalFileReferences( $css, $basepath );
$expected = [ $basepath . 'not-commented.gif' ];
- $this->assertArrayEquals(
+ $this->assertSame(
$expected,
$files,
'Url(...) expression in comment should be omitted.'
/**
* @covers MediaWikiTestCase
* @group MediaWikiTestCaseTest
+ * @group Database
*
* @author Addshore
*/
$this->assertSame( $logger1, $logger2 );
}
+
+ /**
+ * @covers MediaWikiTestCase::setupDatabaseWithTestPrefix
+ * @covers MediaWikiTestCase::copyTestData
+ */
+ public function testCopyTestData() {
+ $this->markTestSkippedIfDbType( 'sqlite' );
+
+ $this->tablesUsed[] = 'objectcache';
+ $this->db->insert(
+ 'objectcache',
+ [ 'keyname' => __METHOD__, 'value' => 'TEST', 'exptime' => $this->db->timestamp( 11 ) ],
+ __METHOD__
+ );
+
+ $lbFactory = MediaWikiServices::getInstance()->getDBLoadBalancerFactory();
+ $lb = $lbFactory->newMainLB();
+ $db = $lb->getConnection( DB_REPLICA, DBO_TRX );
+
+ // sanity
+ $this->assertNotSame( $this->db, $db );
+
+ // Make sure the DB connection has the fake table clones and the fake table prefix
+ MediaWikiTestCase::setupDatabaseWithTestPrefix( $db, $this->dbPrefix(), false );
+
+ $this->assertSame( $this->db->tablePrefix(), $db->tablePrefix(), 'tablePrefix' );
+
+ // Make sure the DB connection has all the test data
+ $this->copyTestData( $this->db, $db );
+
+ $value = $db->selectField( 'objectcache', 'value', [ 'keyname' => __METHOD__ ], __METHOD__ );
+ $this->assertSame( 'TEST', $value, 'Copied Data' );
+ }
+
}